﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Runtime.InteropServices;
using SchemaGen.Attributes;

namespace SchemaGen.Xsd
{
    public class SSimpleType : IEquatable<SSimpleType>
    {
        internal string Name { get; set; }
        internal SDocumentation Documentation { get; set; }
        internal XName BaseType { get; private set; }
        internal List<XElement> Content { get; private set; }
        

        public SSimpleType()
        {
            Content = new List<XElement>();
        }

        /// <summary>
        /// Applies to numeric types like int and decimal
        /// </summary>
        /// <param name="baseType">If null, defaults to minimum's type, maximum's type, or decimal</param>
        public static SSimpleType GetNumericRestriction(object minimum = null, object maximum = null, Type baseType = null, int fractionDigits = -1, int totalDigits = 0, bool exclusiveMinMax = false)
        {
            var result = new SSimpleType();

            if (baseType == null)
            {
                if (minimum != null && maximum != null)
                {
                    baseType = GetLeastRestrictiveType(minimum, maximum);
                    if (baseType == null)
                        throw new SchemaException(string.Format(
                            "Cannot resolve XML type from {0} and {1} because no implicit cast exists from one to the other.",
                            minimum.GetType().FullName, maximum.GetType().FullName));
                }
                else if (minimum != null) baseType = minimum.GetType();
                else if (maximum != null) baseType = maximum.GetType();
                else baseType = typeof(decimal);
            }

            XName baseTypeName;
            if (SNames.ClrTypeLookup.TryGetValue(baseType, out baseTypeName))
                result.BaseType = baseTypeName;
            else
                throw new SchemaException(string.Format("The CLR type {0} has no XML equivalent.", baseType.FullName));

            if (minimum != null)
                result.Content.Add(
                    new XElement(exclusiveMinMax ? SNames.Elements.MinExclusive : SNames.Elements.MinInclusive,
                        new XAttribute("value", minimum)));

            if (maximum != null)
                result.Content.Add(
                    new XElement(exclusiveMinMax ? SNames.Elements.MaxExclusive : SNames.Elements.MaxInclusive,
                        new XAttribute("value", maximum)));

            if (fractionDigits >= 0)
                result.Content.Add(
                    new XElement(SNames.Elements.FractionDigits,
                        new XAttribute("value", fractionDigits)));

            if (totalDigits > 0)
                result.Content.Add(
                    new XElement(SNames.Elements.TotalDigits,
                        new XAttribute("value", fractionDigits)));

            return result;
        }

        public static SSimpleType GetStringLengthRestriction(int minLength = -1, int maxLength = -1)
        {
            var result = new SSimpleType();

            if (maxLength >=0 && 
                minLength >= 0 && 
                maxLength == minLength)
            {
                result.Content.Add(
                    new XElement(SNames.Elements.Length,
                        new XAttribute("value", maxLength)));
            }
            else
            {
                if (maxLength >= 0)
                    result.Content.Add(
                        new XElement(SNames.Elements.MaxLength,
                            new XAttribute("value", maxLength)));

                if (minLength >= 0)
                    result.Content.Add(
                        new XElement(SNames.Elements.MinLength,
                            new XAttribute("value", minLength)));
            }

            return result;
        }

        public static SSimpleType GetEnumerationRestriction(params object[] allowedValues)
        {
            return GetEnumerationRestriction(allowedValues, new string[allowedValues.Length]);
        }

        public static SSimpleType GetEnumerationRestriction(object[] allowedValues, string[] documentation)
        {
            var result = new SSimpleType()
            {
                BaseType = SNames.StringDataTypes.String
            };

            for (int i = 0; i < allowedValues.Length; i++)
            {
                var nextElement = new XElement(SNames.Elements.Enumeration,
                    new XAttribute("value", allowedValues[i]));
                if (documentation[i] != null) nextElement.Add(((SDocumentation)documentation[i]).Create());
                result.Content.Add(nextElement);
            }

            return result;
        }

        public static SSimpleType GetEnumerationRestriction(Type enumeration)
        {
            var enumerationData = enumeration.GetFields()
                .Select(f => new { name = f.Name, metaData = (XmlEnumMetaDataAttribute)f.GetCustomAttributes(typeof(XmlEnumMetaDataAttribute), false).FirstOrDefault() })
                .Where(f => f.metaData == null || !f.metaData.DoNotInclude)
                .SkipWhile(f => f.name == "value__") // Skip the first element, which is a private member of Enum
                .ToArray();

            var enumerationNames = new string[enumerationData.Length];
            var enumerationDocumentation = new string[enumerationData.Length];

            for (int i = 0; i < enumerationData.Length; i++)
            {
                enumerationNames[i] = enumerationData[i].name;
                enumerationDocumentation[i] = enumerationData[i].metaData != null ? enumerationData[i].metaData.Documentation : null;
            }

            return GetEnumerationRestriction(enumerationNames, enumerationDocumentation);
        }

        public static SSimpleType GetPatternRestriction(string pattern)
        {
            var result = new SSimpleType()
            {
                BaseType = SNames.StringDataTypes.String
            };

            throw new NotImplementedException();
        }

        public bool Equals(SSimpleType other)
        {
            return BaseType.Equals(other.BaseType) &&
                Content.SequenceEqual(other.Content);
        }

        internal XElement Create()
        {
            var result = new XElement(SNames.Elements.SimpleType);

            if (Name != null) result.Add(new XAttribute("name", Name));
            
            if (Documentation != null && !string.IsNullOrWhiteSpace(Documentation.Comment))
                result.Add(Documentation.Create());

            var restriction = new XElement(SNames.Elements.Restriction, Content);
            if (BaseType != null) restriction.Add(new XAttribute("base", BaseType));
            result.Add(restriction);

            return result;
        }

        /// <remarks>This is a lazy implementation, but it's not important.</remarks>
        private static Type GetLeastRestrictiveType(object objOne, object objTwo)
        {
            Type oneType = objOne.GetType();
            Type twoType = objTwo.GetType();

            if (oneType.IsValueType && twoType.IsValueType)
            {
                var oneIsFraction = objOne.ToString().Contains('.');
                var twoIsFraction = objTwo.ToString().Contains('.');

                if (oneIsFraction == twoIsFraction)
                    return Marshal.SizeOf(objOne) > Marshal.SizeOf(objTwo) ? oneType : twoType;
                else
                    return typeof(decimal); // Just return the least restrictive type of all if only one's a fraction
            }

            // Not going to bother checking for op_implicit or whatever

            return null;
        }

    }
}
